<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8" />
    <meta name="renderer" content="webkit" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
    <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1" />
    <!-- <meta name="keywords" content="" /> -->
    <!-- <meta name="description" content="" /> -->
    <meta name="author" content="gsw945" />
    <base target="_blank" />
    <title>Demo</title>
    <!-- <link rel="shortcut icon" type="image/x-icon" href="/favicon.ico" /> -->
    <link rel="shortcut icon" type="image/png" href="" />
    <!-- 禁用掉favicon.ico请求，不然每次都会出现404-->
    <!-- <link rel="icon" href="data:;base64,AA==" /> -->
    <link href="https://lf9-cdn-tos.bytecdntp.com/cdn/expire-1-M/xterm/4.11.0/xterm.css" type="text/css" rel="stylesheet" />
    <style type="text/css">
        html,
        body {
            margin: 0;
            padding: 0;
            border: 0;
            background-color: #fff;
            overflow: hidden;
        }
        body {
            margin: 2px;
        }
        div#terminal-container {
            height: calc(100vh - 4px);
            width: calc(100vw - 4px);
        }
    </style>
</head>
<body>
    <!-- page -->
    <div id="terminal-container"></div>
    <!-- docReady -->
    <script type="text/javascript">
        // from: https://stackoverflow.com/questions/9899372/pure-javascript-equivalent-of-jquerys-ready-how-to-call-a-function-when-t/9899701#9899701
        (function (funcName, baseObj) {
            "use strict";
            // The public function name defaults to window.docReady
            // but you can modify the last line of this function to pass in a different object or method name
            // if you want to put them in a different namespace and those will be used instead of
            // window.docReady(...)
            funcName = funcName || "docReady";
            baseObj = baseObj || window;
            var readyList = [];
            var readyFired = false;
            var readyEventHandlersInstalled = false;

            // call this when the document is ready
            // this function protects itself against being called more than once
            function ready() {
                if (!readyFired) {
                    // this must be set to true before we start calling callbacks
                    readyFired = true;
                    for (var i = 0; i < readyList.length; i++) {
                        // if a callback here happens to add new ready handlers,
                        // the docReady() function will see that it already fired
                        // and will schedule the callback to run right after
                        // this event loop finishes so all handlers will still execute
                        // in order and no new ones will be added to the readyList
                        // while we are processing the list
                        readyList[i].fn.call(window, readyList[i].ctx);
                    }
                    // allow any closures held by these functions to free
                    readyList = [];
                }
            }

            function readyStateChange() {
                if (document.readyState === "complete") {
                    ready();
                }
            }

            // This is the one public interface
            // docReady(fn, context);
            // the context argument is optional - if present, it will be passed
            // as an argument to the callback
            baseObj[funcName] = function (callback, context) {
                if (typeof callback !== "function") {
                    throw new TypeError("callback for docReady(fn) must be a function");
                }
                // if ready has already fired, then just schedule the callback
                // to fire asynchronously, but right away
                if (readyFired) {
                    setTimeout(function () { callback(context); }, 1);
                    return;
                } else {
                    // add the function and context to the list
                    readyList.push({ fn: callback, ctx: context });
                }
                // if document already ready to go, schedule the ready function to run
                // IE only safe when readyState is "complete", others safe when readyState is "interactive"
                if (document.readyState === "complete" || (!document.attachEvent && document.readyState === "interactive")) {
                    setTimeout(ready, 1);
                } else if (!readyEventHandlersInstalled) {
                    // otherwise if we don't have event handlers installed, install them
                    if (document.addEventListener) {
                        // first choice is DOMContentLoaded event
                        document.addEventListener("DOMContentLoaded", ready, false);
                        // backup is window load event
                        window.addEventListener("load", ready, false);
                    } else {
                        // must be IE
                        document.attachEvent("onreadystatechange", readyStateChange);
                        window.attachEvent("onload", ready);
                    }
                    readyEventHandlersInstalled = true;
                }
            }
        })("docReady", window);
        // modify this previous line to pass in your own method name and object for the method to be attached to
    </script>
    <!-- xterm -->
    <script src="https://lf9-cdn-tos.bytecdntp.com/cdn/expire-1-M/xterm/4.11.0/xterm.js" type="text/javascript"></script>
    <script src="https://lf6-cdn-tos.bytecdntp.com/cdn/expire-1-M/xterm/4.11.0/addons/fit/xterm-addon-fit.js" type="text/javascript"></script>
    <script src="https://lf3-cdn-tos.bytecdntp.com/cdn/expire-1-M/xterm/4.11.0/addons/attach/xterm-addon-attach.js" type="text/javascript"></script>
    <script src="https://lf9-cdn-tos.bytecdntp.com/cdn/expire-1-M/xterm/4.11.0/addons/search/xterm-addon-search.js" type="text/javascript"></script>
    <script src="https://lf26-cdn-tos.bytecdntp.com/cdn/expire-1-M/xterm/4.11.0/addons/webgl/xterm-addon-webgl.js" type="text/javascript"></script>
    <!-- logic -->
    <script type="text/javascript">
        /**
         * 处理字符串(前后trim掉指定的字符)
         * @param {string} str 需要处理的字符串
         * @param {char} char2Remove 需要移除的字符
         * @returns 处理后的字符串: string
         */
        function TrimChar(str, char2Remove) {
            while (str.charAt(0) == char2Remove) {
                str = str.substring(1);
            }
            while (str.charAt(str.length - 1) == char2Remove) {
                str = str.substring(0, str.length - 1);
            }
            return str;
        }

        /**
         * utf.js - UTF-8 <=> UTF-16 convertion
         *
         * Copyright (C) 1999 Masanao Izumo <iz@onicos.co.jp>
         * Version: 1.0
         * LastModified: Dec 25 1999
         * This library is free.  You can redistribute it and/or modify it.
         * from: https://stackoverflow.com/questions/8936984/uint8array-to-string-in-javascript/22373197#22373197
         */
        function Utf8ArrayToStr(array) {
            var out, i, len, c;
            var char2, char3;

            out = '';
            len = array.length;
            i = 0;
            while (i < len) {
                c = array[i++];
                switch (c >> 4) {
                    case 0:
                    case 1:
                    case 2:
                    case 3:
                    case 4:
                    case 5:
                    case 6:
                    case 7:
                        // 0xxxxxxx
                        out += String.fromCharCode(c);
                        break;
                    case 12:
                    case 13:
                        // 110x xxxx   10xx xxxx
                        char2 = array[i++];
                        out += String.fromCharCode(((c & 0x1f) << 6) | (char2 & 0x3f));
                        break;
                    case 14:
                        // 1110 xxxx  10xx xxxx  10xx xxxx
                        char2 = array[i++];
                        char3 = array[i++];
                        out += String.fromCharCode(
                            ((c & 0x0f) << 12) | ((char2 & 0x3f) << 6) | ((char3 & 0x3f) << 0),
                        );
                        break;
                    default:
                        console.warn(c);
                        break;
                }
            }
            return out;
        }

        function BinaryArray2JSON(binArray) {
            /*
            // 方案一(存在中文乱码问题)
            var str = ""
            for (var i = 0 i < binArray.length i++) {
                str += String.fromCharCode(parseInt(binArray[i]))
            }
            */
            // 方案二
            var str = Utf8ArrayToStr(binArray);
            try {
                return JSON.parse(str);
            } catch (e) {
                console.log(e);
                return str;
            }
        }

        function JSON2Array(json) {
            var str = JSON.stringify(json, null, 0);
            var arr = [];
            for (var i = 0; i < str.length; i++) {
                var code = str.charCodeAt(i);
                var encoded = encodeURIComponent(str[i]);
                if (encoded.indexOf('%') >= 0) {
                    encoded = TrimChar(encoded, '%');
                    var codeList = encoded.split('%').map(item => parseInt(item, 16));
                    for (var j = 0; j < codeList.length; j++) {
                        arr.push(codeList[j]);
                    }
                } else {
                    arr.push(code);
                }
            }
            var ret = new Uint8Array(arr);
            return ret.buffer;
        }

        /**
         * 准备数据
         * @param {object} data 需要发送的数据
         * @param {boolean} isBinary WebSocket消息是否采用二进制[Binary]方式(否则为文本[Text]模式)
         * @returns 二进制通信: Uint8Array，文本通信: string
         */
        function prepareData(data, isBinary) {
            if (!data) {
                return '';
            }
            if (data instanceof String) {
                return data;
            } else {
                return isBinary ? JSON2Array(data) : JSON.stringify(data, null, 0);
            }
        }

        docReady(function () {
            console.log('hello pty');

            // ref: https://xtermjs.org/js/demo.js
            var term = new Terminal({
                cols: 80, // 列数
                rows: 30, // 行数
                cursorBlink: true, // 光标闪烁
                cursorStyle: 'underline',  // 光标样式(block、underline、bar)
                rendererType: 'canvas', // 渲染器类型
                theme: {
                    background: 'darkslategray',
                    foreground: 'orange'
                }
            });

            // 自适应插件
            var fitAddon = new FitAddon.FitAddon();
            term.loadAddon(fitAddon);

            // Open the terminal in #terminal-container
            term.open(document.getElementById('terminal-container'), true);
            term.focus();

            // Make the terminal's size and geometry fit the size of #terminal-container
            fitAddon.fit();

            var schema = (/^https/ig).test(document.location.protocol) ? 'wss' : 'ws';
            var url = `${schema}://${document.location.host}/terminal`;
            console.log(url);

            // Create WebSocket connection.
            var socket = new WebSocket(url);

            // ref: https://xtermjs.org/docs/api/addons/attach/
            var attachAddon = null;

            // Connection opened
            socket.addEventListener('open', function (event) {
                // socket.send('Hello Server!');
                console.log('[ws-client]: <connected>');

                attachAddon = new AttachAddon.AttachAddon(socket);
                // Attach the socket to term
                term.loadAddon(attachAddon);

                // 限制上报频率
                notifyInterval = setInterval(function () {
                    if (newestSize[0] != latestNotifySize[0] || newestSize[1] != latestNotifySize[1]) {
                        resizeNotify(newestSize[0], newestSize[1]);
                    }
                }, 333);
            });

            // Listen for messages
            socket.addEventListener('message', function (event) {
                console.log('[ws-server]:', event.data);
            });

            // Connection closed
            socket.addEventListener('close', function (event) {
                console.log(event);
                if (notifyInterval !== null) {
                    clearInterval(notifyInterval);
                    notifyInterval = null;
                }
                alert('closed');
            });

            // socket error
            socket.addEventListener('error', function (event) {
                console.log(event);
                if (notifyInterval !== null) {
                    clearInterval(notifyInterval);
                    notifyInterval = null;
                }
                alert('error');
            });

            // 控制键盘按键是否被xterm捕获
            term.attachCustomKeyEventHandler(function (event) {
                // 只有当WebSocket连接后才允许xterm捕获按键
                return socket.readyState === WebSocket.OPEN;
            });

            // size上报interval
            var notifyInterval = null;
            // 最新上报的size
            var latestNotifySize = [0, 0];
            // 最新变化的size
            var newestSize = [term.cols, term.rows];

            /**
             * 通知服务器修改pty终端尺寸
             * @param cols
             * @param rows
             */
            function resizeNotify(cols, rows) {
                if (socket.readyState === WebSocket.OPEN) {
                    latestNotifySize = [cols, rows];
                    socket.send(prepareData({
                        action: 'resize',
                        data: {
                            cols: cols,
                            rows: rows
                        }
                    }, true));
                }
            }

            // 终端尺寸改变回调
            term.onResize(function (size) {
                console.log('size:', size);
                newestSize = [size.cols, size.rows];
            });

            window.addEventListener('resize', function (event) {
                fitAddon.fit();
                // console.log(`term: ${term.cols}x${term.rows}`);
                // console.log(`inner: ${window.innerWidth}x${window.innerHeight}; outer: ${window.outerWidth}x ${window.outerHeight}`);
            });
        });
    </script>
</body>
</html>